想像一家餐廳。傳統伺服器像是為每一桌客人都聘請一位專屬服務生(多執行緒)。如果客人看菜單看很久,服務生只能乾站在旁邊等,浪費極大的人事與記憶體成本。
Node.js 只有「一位」超級服務生(單執行緒),但他絕對不等待。幫第一桌點完餐送進廚房(發出 I/O 請求)後,立刻轉身去幫第二桌點餐。當廚房把第一桌的菜做好(非同步任務完成),會按鈴通知服務生來端菜(觸發 Event 與 Callback)。
這個不斷巡視客人與廚房的無限迴圈,就是 Event Loop。它讓 Node.js 能用極少的系統資源,同時處理成千上萬個使用者的連線。
過去,為了處理非同步任務完成後的動作,我們必須把函式當作參數傳遞(Callback),這很容易導致一層包一層的「波動拳」程式碼(Callback Hell)。後來出現了 Promise,將非同步任務包裝成一個「承諾」,用語法鏈 .then() 來處理。
最終,現代 JavaScript 迎來了終極降維打擊:async/await。
它讓你能用寫同步程式的直覺語法,來執行非同步的邏輯。
我們將使用 Node.js 內建的 fs/promises(檔案系統)模組,來模擬一個耗時的檔案讀取與寫入過程,徹底體會 Event Loop 的運作順序。
步驟 1:建立測試資料
在你的 hello-node 資料夾下,新增一個文字檔 data.txt,裡面隨便寫一行字:
這是一份來自戰錘40K的機檔案。
步驟 2:撰寫非同步邏輯
清空 app.js,將內容換成以下程式碼並存檔:
// 使用內建的 promises 版本模組,原生支援 async/await
import { readFile, writeFile } from 'fs/promises';
// 模擬一個會耗時的「暫停」函式 (利用 Promise 與 setTimeout)
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
// 在函式前方加上 async,標記內部包含非同步操作
const processData = async () => {
try {
console.log("1. 準備讀取檔案...");
// await 會暫停這個函式內部的執行,直到讀取完成
// 關鍵:這「不會」卡住整個 Node.js 主執行緒
const content = await readFile('./data.txt', 'utf-8');
console.log(`2. 讀取成功,內容為:${content}`);
console.log("3. 模擬資料庫處理中 (強制暫停 2 秒)...");
await delay(2000);
const newContent = content + " \n-> 已被 Node.js 非同步處理完畢!";
console.log("4. 準備寫入新檔案...");
await writeFile('./output.txt', newContent, 'utf-8');
console.log("5. 檔案寫入完成!請查看資料夾的 output.txt");
} catch (error) {
// async/await 最棒的地方在於,可以用傳統的 try...catch 捕捉非同步錯誤
console.error(" 發生錯誤:", error.message);
}
};
console.log("--- 程式開始執行 ---");
processData(); // 呼叫非同步函式
console.log("--- 程式主線已跑完,等待非同步任務 ---");
步驟 3:執行與觀察
在終端機執行 node app.js,請仔細觀察終端機印出文字的順序與時間差。
你會發現順序是這樣的:
--- 程式開始執行 ---
1. 準備讀取檔案...
--- 程式主線已跑完,等待非同步任務 ---
2. 讀取成功...
3. 模擬資料庫處理中...
4. 準備寫入新檔案...
5. 檔案寫入完成!
這就是單執行緒不阻塞的完美證明!
當 Node.js 執行到 await readFile 時,它把這個耗時任務丟給作業系統,主執行緒立刻往下跑,印出了最後一行的「程式主線已跑完」。直到作業系統讀完檔案,Event Loop 才會回頭把 async 函式內部剩下的程式碼執行完畢。